<?php
namespace Easy\Db\Driver;

use Easy\Db\DbDTO;
use Easy\Db\DbPage;
use Easy\Db\PdoParamDTO;
use Easy\Db\XDbDriver;
use Easy\EasyLog;

/**
 * PDO数据库驱动
 */
class XPdo extends XDbDriver
{
    const CONN_MASTER = 1;//主库链接
    const CONN_SLAVE  = 2;//从库链接
    const SQL_INSERT  = 1;//插入
    const SQL_UPDATE  = 2;//更新
    const SQL_DELETE  = 3;//删除

    protected $_PDOStatement = null;
    protected $_fetchMode    = \PDO::FETCH_ASSOC;
    protected $_lastSql      = [];
    protected $_lastBind     = '';
    protected $_dbDTO        = null;
    protected $_pdoParamDTO  = null;
    private   $_linkID       = null;
    private   $_currentLink  = null;
    private   $_transTimes   = 0;//事务
    private   $_disableSlave = false;//是否禁用从库
    private   $_noexecute    = false;

    public function __construct(DbDTO $dbDTO, PdoParamDTO $paramDTO)
    {
        $this->setDb($dbDTO, $paramDTO);
    }


    private function setDb(DbDTO $dbDTO, PdoParamDTO $paramDTO)
    {
        $dbDTO->slaveHost     = is_null($dbDTO->slaveHost) ? $dbDTO->masterHost : $dbDTO->slaveHost;
        $dbDTO->port          = is_null($dbDTO->port) ? '3306' : $dbDTO->port;
        $this->_dbDTO         = $dbDTO;
        $paramDTO->persistent = is_null($paramDTO->persistent) ? false : $paramDTO->persistent;
        $paramDTO->charset    = is_null($paramDTO->charset) ? 'utf-8' : $paramDTO->charset;
        $paramDTO->prepare    = is_null($paramDTO->prepare) ? false : $paramDTO->prepare;
        $this->_pdoParamDTO   = $paramDTO;
    }

    protected function doConnect($linkType = self::CONN_MASTER)
    {
        $dbDTO    = $this->_dbDTO;
        $dbParams = $this->_pdoParamDTO;
        $host     = $linkType == self::CONN_MASTER ? $dbDTO->masterHost : $dbDTO->slaveHost;
        $linkType = $linkType == self::CONN_MASTER ? self::CONN_MASTER : self::CONN_SLAVE;
        $dsn      = 'mysql:dbname=' . $dbDTO->name . ';host=' . $host . ';port=' . $dbDTO->port;
        if (!isset($this->_linkID[$linkType]) || !$this->_linkID[$linkType]) {
            $options[\PDO::ATTR_PERSISTENT]                   = $dbParams->persistent;
            $options['params'][\PDO::ATTR_EMULATE_PREPARES]   = $dbParams->prepare;
            $options['params'][\PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES ' . $dbDTO->charset;
            try {
                $this->_linkID[$linkType] = new \PDO($dsn, $dbDTO->user, $dbDTO->passwd, $options);
            } catch (\PDOException $e) {
                E($e->getMessage());
            }
            $this->_linkID[$linkType]->exec('SET NAMES ' . $dbDTO->charset);
        }

        return $this->_linkID[$linkType];
    }


    public function setDisableSlave()
    {
        $this->_disableSlave = true;

        return $this;
    }

    public function getLastSql()
    {
        return $this->_lastSql;
    }

    /**
     * 释放查询结果
     *
     * @access public
     */
    public function free()
    {
        $this->_PDOStatement = null;
    }


    private function query($sql, $bind = [])
    {
        if (!is_array($bind)) {
            $bind = [$bind];
        }
        $explainSql = $this->buildLog($sql, $bind);
        EasyLog::log($explainSql, '_sql');
        $this->_lastSql[] = $explainSql;
        try {
            if ($this->_noexecute) {
                throw new \Exception($explainSql);
            }
            $this->_PDOStatement = $this->_currentLink->prepare($sql);
            $this->_PDOStatement->execute($bind);
            $errorInfo = $this->_PDOStatement->errorInfo();
            if ($errorInfo[0] != '00000') {
                throw new \PDOException($errorInfo[2], $errorInfo[1]);
            }
            $this->_PDOStatement->setFetchMode(\PDO::FETCH_ASSOC);
            $this->cleanWhere();

            return $this->_PDOStatement;
        } catch (\PDOException $e) {
            throw new \Exception($e->getMessage() . ':' . $explainSql);

            return false;
        }
    }

    /**
     * 启动事务
     *
     * @access public
     * @return void
     */
    public function startTrans()
    {
        if (!isset($this->_linkID[self::CONN_MASTER])) {
            return false;
        }
        if ($this->_transTimes == 0) {
            $this->_linkID[self::CONN_MASTER]->beginTransaction();
        }
        $this->_transTimes++;

        return;
    }

    /**
     * 用于非自动提交状态下面的查询提交
     *
     * @access public
     * @return boolen
     */
    public function commit()
    {
        if ($this->_transTimes > 0) {
            $result            = $this->_linkID[self::CONN_MASTER]->commit();
            $this->_transTimes = 0;
            if (!$result) {
                return false;
            }
        }

        return true;
    }

    /**
     * 事务回滚
     *
     * @access public
     * @return boolen
     */
    public function rollback()
    {
        if ($this->_transTimes > 0) {
            $result            = $this->_linkID[self::CONN_MASTER]->rollback();
            $this->_transTimes = 0;
            if (!$result) {
                return false;
            }
        }

        return true;
    }


    /**
     * 关闭数据库
     *
     * @access public
     */
    public function close()
    {
        $this->_linkID = null;
    }


    //获取分页查询结果
    private function fetchPage($sql, DbPage $pageObj, $values = [])
    {
        $countSql = preg_replace("/^select /", "select SQL_CALC_FOUND_ROWS ", strtolower(trim($sql)));
        $countSql = $countSql . ' limit 1';
        $this->query($countSql, $values);
        $stmt  = $this->query('SELECT FOUND_ROWS() as count');
        $count = $stmt->fetchColumn(0);
        if (!$pageObj->isInit) {
            $pageObj->initTotalRows($count);
        }
        if ($count) {
            $sql   = $sql . $pageObj->toLimitStr();
            $stmt  = $this->query($sql, $values);
            $datas = $this->getListArr($stmt);
        } else {
            $datas = [];
        }
        $pageObj->setData($datas);

        return $pageObj->getResult();
    }

    private function getListArr($stmt)
    {
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }

    /**
     * @param string $sql
     * @param array $where
     *
     * @return mixed 获取所有查询结果、支持分页
     */
    public function fetchAll($sql = '', $where = [])
    {
        $this->_currentLink = $this->setCurLink(self::CONN_SLAVE);
        list($sqlQuery, $values) = $this->prepareQuery($sql, $where);
        $limit = $this->getLimit();
        if ($limit && is_array($limit)) {
            list($start, $limit) = $this->getLimit();
            $pageObj = DbPage::limit($start, $limit);

            return $this->fetchPage($sqlQuery, $pageObj, $values);
        } else {
            if ($limit) {
                $sqlQuery .= ' limit ' . $limit;
            }
            $stmt = $this->query($sqlQuery, $values);

            return $this->getListArr($stmt);
        }
    }

    /**
     * 获取一行结果
     *
     * @param string $sql
     * @param array $where
     *
     * @return array()
     */
    public function fetchRow($sql = '', $where = [])
    {
        $this->_currentLink = $this->setCurLink(self::CONN_SLAVE);
        list($sqlQuery, $values) = $this->prepareQuery($sql, $where);
        if (strpos(strtolower($sqlQuery), ' limit ') === false) {
            $sqlQuery .= ' limit 1';
        }
        $stmt = $this->query($sqlQuery, $values);

        return $stmt->fetch(\PDO::FETCH_ASSOC);
    }

    public function fetchOne($sql = '', $where = [])
    {
        $this->_currentLink = $this->setCurLink(self::CONN_SLAVE);
        list($sqlQuery, $values) = $this->prepareQuery($sql, $where);
        if (strpos(strtolower($sqlQuery), ' limit ') === false) {
            $sqlQuery .= ' limit 1';
        }
        $stmt = $this->query($sqlQuery, $values);
        $data = $stmt->fetchColumn(0);

        return $data;
    }

    public function fetchPairs($sql = '', $where = [])
    {
        $this->_currentLink = $this->setCurLink(self::CONN_SLAVE);
        $datas              = $this->fetchAll($sql, $where);
        $newDatas           = [];
        if (isset($datas['datas'])) {
            $datas = $datas['datas'];
        }
        foreach ($datas as $v) {
            if (count($v) < 3) {
                $v1            = array_shift($v);
                $newDatas[$v1] = array_shift($v);
            } else {
                $v1            = reset($v);
                $newDatas[$v1] = $v;
            }
        }

        return $newDatas;
    }


    public function insert(array $values)
    {
        list($sql, $values) = $this->getColumnSql(self::SQL_INSERT, $values);
        $this->_currentLink = $this->setCurLink(self::CONN_MASTER);
        $stmt               = $this->query($sql, $values);
        $result             = $stmt->rowCount();
        if ($result !== false) {
            $lastInserId = $this->_currentLink->lastInsertId();
            if ($lastInserId) {
                $result = $lastInserId;
            }
        }

        return $result;
    }

    public function update(array $values, $limit = 0, $where = [])
    {
        list($sql, $values) = $this->getColumnSql(self::SQL_UPDATE, $values, $where);
        if ($limit > 0) {
            $sql .= ' limit ' . $limit;
        }
        $this->_currentLink = $this->setCurLink(self::CONN_MASTER);
        $stmt               = $this->query($sql, $values);
        $result             = $stmt->rowCount();

        return $result;
    }

    public function delete($limit = 0, $where = [])
    {
        list($sql, $values) = $this->getColumnSql(self::SQL_DELETE, [], $where);
        if ($limit > 0) {
            $sql .= ' limit ' . $limit;
        }
        $this->_currentLink = $this->setCurLink(self::CONN_MASTER);
        $stmt               = $this->query($sql, $values);
        $result             = $stmt->rowCount();

        return $result;
    }

    public function execute($sql)
    {
        $this->_currentLink = $this->setCurLink(self::CONN_MASTER);
        $stmt               = $this->query($sql);
        $result             = $stmt->rowCount();

        return $result;
    }

    public function setInc($field, $step = 1)
    {
        return $this->setField($field, '`' . $field . '`+' . $step);
    }

    public function setDec($field, $step = 1)
    {
        return $this->setField($field, '`' . $field . '`-' . $step);
    }

    public function setNoExe()
    {
        $this->_noexecute = true;

        return $this;
    }

    public function updateField($field, $value)
    {
        return $this->setField($field, $value);
    }

    private function setField($field, $value)
    {
        $data[$field] = $value;

        return $this->update($data);
    }

    private function setCurLink($linkType)
    {
        $linkType = $this->_disableSlave ? self::CONN_MASTER : $linkType;

        return $this->doConnect($linkType);
    }

    private function cleanWhere()
    {
        $this->_fields   = [];
        $this->_values   = [];
        $this->_orFields = [];
        $this->_whereStr = '';
    }

    private function buildLog($sql, $values = [])
    {
        if (!empty($values)) {
            $logsql = str_replace('%', '#', $sql);
            $logsql = str_replace('?', '%s', $logsql);
            $logsql = vsprintf($logsql, self::stdSqlValues($values));
            $logsql = str_replace('#', '%', $logsql);

            return $logsql;
        } else {
            return $sql;
        }
    }

    private function stdSqlValues($arr)
    {
        $lists = [];
        foreach ($arr as $key => $item) {
            if (is_string($item)) {
                $lists[$key] = "'$item'";
            } elseif (is_null($item)) {
                $lists[$key] = "null";
            } else {
                $lists[$key] = $item;
            }
        }

        return $lists;
    }

    private function getColumnSql($handle, array $values, $where = [])
    {
        $this->sqlKeyFilter($values);
        $sql       = null;
        $newValues = [];
        switch ($handle) {
            case self::SQL_INSERT:
                $column    = [];
                $data      = [];
                $newValues = [];
                foreach ($values as $k => $val) {
                    $column[]    = $k;
                    $data[]      = '?';
                    $newValues[] = $val;
                }
                $sql = 'insert ' . $this->_table . ' (' . implode(',', $column) . ') values(' . implode(',',
                        $data) . ')';
                break;
            case self::SQL_UPDATE:
                $column    = [];
                $newValues = [];
                foreach ($values as $k => $val) {
                    if (strpos($val, '`') === 0) {
                        $column[] = $k . '=' . $val;
                        continue;
                    }
                    $column[]    = $k . '=?';
                    $newValues[] = $val;
                }
                if (!empty($where)) {
                    $this->where($where);
                }
                list($where, $whereVal) = $this->getWhere();
                foreach ($whereVal as $val) {
                    $newValues[] = $val;
                }
                $sql = 'update ' . $this->_table . ' set ' . implode(',', $column) . ' ' . $where;
                break;
            case self::SQL_DELETE:
                if (!empty($where)) {
                    $this->where($where);
                }
                list($where, $whereVal) = $this->getWhere();
                foreach ($whereVal as $val) {
                    $newValues[] = $val;
                }
                $sql = 'delete from ' . $this->_table . $where;
                break;
        }

        return [$sql, $newValues];
    }
}